/*
 * Copyright (c) 2010-2023. Axon Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.axonframework.spring.eventsourcing;

import org.axonframework.config.Configuration;
import org.axonframework.eventhandling.DomainEventMessage;
import org.axonframework.eventsourcing.AbstractAggregateFactory;
import org.axonframework.eventsourcing.AggregateFactory;
import org.axonframework.eventsourcing.IncompatibleAggregateException;
import org.axonframework.modelling.command.inspection.AggregateModel;
import org.axonframework.modelling.command.inspection.AnnotatedAggregateMetaModelFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;

import static java.lang.String.format;

/**
 * An {@link AggregateFactory} implementation that uses Spring prototype beans to create new uninitialized instances of
 * Aggregates.
 *
 * @param <T> The type of aggregate generated by this aggregate factory.
 * @author Allard Buijze
 * @since 1.2
 */
public class SpringPrototypeAggregateFactory<T>
        implements AggregateFactory<T>, InitializingBean, ApplicationContextAware, BeanNameAware {

    private final Class<T> aggregateType;
    private final String prototypeBeanName;
    private final Map<Class<? extends T>, String> subtypes;

    private ApplicationContext applicationContext;
    private String beanName;
    private AggregateFactory<T> delegate;

    /**
     * Initializes the factory to create beans instances for the bean with given {@code prototypeBeanName}.
     * <p>
     * Note that the bean should have the prototype scope.
     *
     * @param prototypeBeanName the name of the prototype bean this repository serves.
     * @deprecated In favor of {@link #SpringPrototypeAggregateFactory(Class, String, Map)} to ensure all required
     * fields are present for both polymorphic and non-polymorphic aggregates.
     */
    @Deprecated
    public SpringPrototypeAggregateFactory(String prototypeBeanName) {
        this(prototypeBeanName, new HashMap<>());
    }

    /**
     * Initializes the factory to create beans instances for the bean with given {@code prototypeBeanName} and its
     * {@code subtypes}.
     * <p>
     * Note that the bean should have the prototype scope.
     *
     * @param prototypeBeanName the name of the prototype bean this repository serves.
     * @param subtypes          the map of subtype of this aggregate to its spring prototype name
     * @deprecated In favor of {@link #SpringPrototypeAggregateFactory(Class, String, Map)} to ensure all required
     * fields are present for both polymorphic and non-polymorphic aggregates.
     */
    @Deprecated
    public SpringPrototypeAggregateFactory(String prototypeBeanName, Map<Class<? extends T>, String> subtypes) {
        this(null, prototypeBeanName, subtypes);
    }

    /**
     * Initializes the factory to create bean instances from the given {@code prototypeBeanName} or its
     * {@code subtypes}.
     * <p>
     * Whenever {@code subtypes} is not null, it is expected that the {@code prototypeBeanName} <b>does not</b> exist in
     * the {@link ApplicationContext} due to the fact that root types are expected to be {@code abstract}. Furthermore,
     * {@code abstract} classes are not made a part of the {@code ApplicationContext} and as such there cannot be a bean
     * definition referencing it.
     * <p>
     * Note that the {@code prototypeBeanName} should have the prototype scope when there are not {@code subtypes}. When
     * {@code subtypes} are present, all values of the {@code subtypes} collection are expected to be prototype scoped.
     *
     * @param aggregateType     The type of aggregate constructed by this {@link AggregateFactory}. Is the root type of
     *                          the aggregate whenever {@code subtypes} is not empty.
     * @param prototypeBeanName The name of the prototype bean this repository serves. When {@code subtypes} isn't
     *                          empty, the name is expected to equal the {@link Class#getSimpleName() simple name} of
     *                          the {@code aggregateType}.
     * @param subtypes          The map of subtype of this aggregate to its spring prototype name. @return An
     *                          initialized instance of this factory.
     */
    public SpringPrototypeAggregateFactory(Class<T> aggregateType,
                                           String prototypeBeanName,
                                           Map<Class<? extends T>, String> subtypes) {
        this.aggregateType = aggregateType;
        this.prototypeBeanName = prototypeBeanName;
        this.subtypes = subtypes;
    }

    /**
     * Initializes the factory to create bean instances from the given {@code prototypeBeanName} or its
     * {@code subtypes}.
     * <p>
     * Whenever {@code subtypes} is not null, it is expected that the {@code prototypeBeanName} <b>does not</b> exist in
     * the {@link ApplicationContext} due to the fact that root types are expected to be {@code abstract}. Furthermore,
     * {@code abstract} classes are not made a part of the {@code ApplicationContext} and as such there cannot be a bean
     * definition referencing it.
     * <p>
     * Note that the {@code prototypeBeanName} should have the prototype scope when there are not {@code subtypes}. When
     * {@code subtypes} are present, all values of the {@code subtypes} collection are expected to be prototype scoped.
     * <p>
     * This static factory method is provided as an alternative to avoid warnings and errors on ambiguous constructor
     * resolution when using Spring AOT.
     *
     * @param aggregateType     The type of aggregate constructed by this {@link AggregateFactory}. Is the root type of
     *                          the aggregate whenever {@code subtypes} is not empty.
     * @param prototypeBeanName The name of the prototype bean this repository serves. When {@code subtypes} isn't
     *                          empty, the name is expected to equal the {@link Class#getSimpleName() simple name} of
     *                          the {@code aggregateType}.
     * @param subtypes          The map of subtype of this aggregate to its spring prototype name. @return An
     *                          initialized instance of this factory.
     */
    @SuppressWarnings("unused")
    public static <T> SpringPrototypeAggregateFactory<T> withSubtypeSupport(
            Class<T> aggregateType,
            String prototypeBeanName,
            Map<Class<? extends T>, String> subtypes
    ) {
        return new SpringPrototypeAggregateFactory<>(aggregateType, prototypeBeanName, subtypes);
    }

    @Override
    public T createAggregateRoot(String aggregateIdentifier, DomainEventMessage<?> firstEvent) {
        return delegate.createAggregateRoot(aggregateIdentifier, firstEvent);
    }

    @Override
    public Class<T> getAggregateType() {
        return aggregateType;
    }

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setBeanName(@Nonnull String beanName) {
        this.beanName = beanName;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void afterPropertiesSet() {
        // Only when there are no subtypes can we be certain the set prototypeBeanName is an existing bean definition.
        // This stems from the intent that the root of a polymorphic aggregate is abstract, leading to no bean definition.
        if (subtypes.isEmpty() && !applicationContext.isPrototype(prototypeBeanName)) {
            throw new IncompatibleAggregateException(format(
                    "Cannot initialize repository '%s'. "
                            + "The bean with name '%s' does not have the 'prototype' scope.",
                    beanName, prototypeBeanName
            ));
        }

        AggregateModel<T> model;
        if (applicationContext.getBeanNamesForType(Configuration.class).length > 0) {
            Configuration configuration = applicationContext.getBean(Configuration.class);
            model = AnnotatedAggregateMetaModelFactory.inspectAggregate(getAggregateType(),
                                                                        configuration.parameterResolverFactory(),
                                                                        configuration
                                                                                .handlerDefinition(getAggregateType()),
                                                                        subtypes.keySet());
        } else {
            model = AnnotatedAggregateMetaModelFactory.inspectAggregate(getAggregateType(),
                                                                        subtypes.keySet());
        }
        this.delegate = new AbstractAggregateFactory<T>(model) {
            @Override
            protected T doCreateAggregate(String aggregateIdentifier,
                                          // Suppress warning as the abstract doCreateAggregate does so too.
                                          @SuppressWarnings("rawtypes") DomainEventMessage firstEvent) {
                return (T) applicationContext.getBean(prototype(firstEvent.getType()));
            }

            private String prototype(String aggregateType) {
                return aggregateModel().type(aggregateType)
                                       .map(subtypes::get)
                                       // switch to main type for:
                                       // 1. When there are no subtypes, thus we're not dealing with a polymorphic aggregate.
                                       // 2. Backwards compatibility, in cases where firstEvent does not contain the aggregate type.
                                       .orElse(prototypeBeanName);
            }

            @Override
            protected T postProcessInstance(T aggregate) {
                applicationContext.getAutowireCapableBeanFactory()
                                  .configureBean(aggregate, beanNameFor(aggregate.getClass()));
                return aggregate;
            }

            private String beanNameFor(Class<?> aggregateClass) {
                return aggregateClass != aggregateType ? subtypes.get(aggregateClass) : prototypeBeanName;
            }
        };
    }
}
